package org.jarx.android.reader;

import java.io.IOException;
import java.util.List;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;

public class ReaderManager {

    public static final String TAG = "ReaderManager";

    public static ReaderManager newInstance(Context context) {
        return new ReaderManager(context);
    }

    public static Subscription getSubBy(Context c, String column,
            String value) {
        ContentResolver cr = c.getContentResolver();
        String w = column + " = ?";
        String[] wa = new String[]{value};
        Cursor csr = cr.query(Subscription.CONTENT_URI, null, w, wa, null);
        if (csr == null) {
            return null;
        }
        try {
            if (csr.moveToNext()) {
                return new Subscription.FilterCursor(csr).getSubscription();
            } else {
                return null;
            }
        } finally {
            csr.close();
        }
    }

    public static Subscription getSubBy(Context c, Uri uri) {
        ContentResolver cr = c.getContentResolver();
        Cursor csr = cr.query(uri, null, null, null, null);
        if (csr == null) {
            return null;
        }
        try {
            if (csr.moveToNext()) {
                return new Subscription.FilterCursor(csr).getSubscription();
            } else {
                return null;
            }
        } finally {
            csr.close();
        }
    }

    public static Tag getTagBy(Context c, String column, String value) {
        ContentResolver cr = c.getContentResolver();
        String w = column + " = ?";
        String[] wa = new String[]{value};
        Cursor csr = cr.query(Tag.CONTENT_URI, null, w, wa, null);
        if (csr == null) {
            return null;
        }
        try {
            if (csr.moveToNext()) {
                return new Tag.FilterCursor(csr).getTag();
            } else {
                return null;
            }
        } finally {
            csr.close();
        }
    }

    public static Item getItemBy(Context c, String column, String value) {
        ContentResolver cr = c.getContentResolver();
        String where = column + " = ?";
        String[] whereArgs = new String[]{value};
        Cursor csr = cr.query(Item.CONTENT_URI, null, where, whereArgs, null);
        if (csr == null) {
            return null;
        }
        try {
            if (csr.moveToNext()) {
                return new Item.FilterCursor(csr).getItem();
            } else {
                return null;
            }
        } finally {
            csr.close();
        }
    }

    public static Item getItemBy(Context c, Uri uri) {
        ContentResolver cr = c.getContentResolver();
        Cursor csr = cr.query(uri, null, null, null, null);
        if (csr == null) {
            return null;
        }
        try {
            if (csr.moveToNext()) {
                return new Item.FilterCursor(csr).getItem();
            } else {
                return null;
            }
        } finally {
            csr.close();
        }
    }

    public static boolean isStarred(Context c, long itemId) {
        ContentResolver cr = c.getContentResolver();
        String[] s = new String[]{"tag._id"};
        String w = "tag2item.tag_uid = tag.uid and tag2item.action >= 0 and tag2item.item_id = ? and tag.type = " + Tag.TYPE_TAG_STARRED;
        String[] wa = new String[]{String.valueOf(itemId)};
        Cursor csr = cr.query(Tag2Item.CONTENT_URI, s, w, wa, " limit 1");
        try {
            return (csr.getCount() > 0);
        } finally {
            csr.close();
        }
    }

    public static Query getSubQueryByTagUid(String tagUid, boolean unreadOnly) {
        StringBuilder buff = new StringBuilder(128);
        buff.append(Tag2Sub.TABLE_NAME + "." + Tag2Sub._SUB_ID + " = "
            + Subscription.TABLE_NAME + "." + Subscription._ID);
        buff.append(" and ");
        buff.append(Tag2Sub.TABLE_NAME + "." + Tag2Sub._TAG_UID + " = ?");
        if (unreadOnly) {
            buff.append(" and ");
            buff.append(Subscription.TABLE_NAME + "." + Tag._UNREAD_COUNT)
                .append(" > 0");
        }
        String[] wa = new String[]{tagUid};
        return new Query(Subscription.CONTENT_URI, null, new String(buff), wa, null);
    }

    public static Query getItemQueryBySubId(long subId, boolean unreadOnly) {
        StringBuilder buff = new StringBuilder("sub_id = ?");
        if (unreadOnly) {
            buff.append(" and read = 0");
        }
        String[] wa = new String[]{String.valueOf(subId)};
        return new Query(Item.CONTENT_URI, null, new String(buff), wa, null);
    }

    public static Query getItemQueryByTagUid(String tagUid, boolean unreadOnly) {
        StringBuilder buff = new StringBuilder(
            "tag2item.item_id = item._id and tag2item.tag_uid = ? and tag2item.action >= 0");
        if (unreadOnly) {
            buff.append(" and item.read = 0");
        }
        String[] wa = new String[]{tagUid};
        return new Query(Item.CONTENT_URI, null, new String(buff), wa, null);
    }

    public static Query getItemQueryBySubTagUid(String tagUid, boolean unreadOnly) {
        StringBuilder buff = new StringBuilder(
            "item.sub_id in (select tag2sub.sub_id from tag2sub where tag2sub.tag_uid = ?)");
        if (unreadOnly) {
            buff.append(" and item.read = 0");
        }
        String[] wa = new String[]{tagUid};
        return new Query(Item.CONTENT_URI, null, new String(buff), wa, null);
    }

    public static Query getItemQueryAll(boolean unreadOnly) {
        String w = (unreadOnly) ? "read = 0": null;
        return new Query(Item.CONTENT_URI, null, w, null, null);
    }

    private final Context context;
    private ReaderClient client;

    private ReaderManager(Context context) {
        this.context = context;
    }

    public ReaderClient getClient() throws IOException, ReaderException {
        if (this.client == null) {
            this.client = new GoogleReaderClient(this.context);
            this.client.login();
        }
        return this.client;
    }

    public int sync() throws IOException, ReaderException {
        long syncTime = System.currentTimeMillis();

        syncReads(syncTime);
        syncSubs(syncTime);

        int count = 0;

        ContentResolver cr = this.context.getContentResolver();
        String w = Subscription._ITEM_SYNC_TIME + " = 0 or "
             + Subscription._NEWEST_ITEM_TIME + " > "
             + Subscription._ITEM_SYNC_TIME;
        Subscription.FilterCursor csr = new Subscription.FilterCursor(
            cr.query(Subscription.CONTENT_URI, null, w, null, null));
        try {
            while (csr.moveToNext()) {
                Subscription sub = csr.getSubscription();
                Log.d(TAG, "[DEBUG] sync items " + sub.getUid());
                try {
                    count += syncItems(sub, syncTime);
                } catch (ReaderException e) {
                    Utils.logError(this.context, e);
                }
            }
        } finally {
            csr.close();
        }

        removeLocalReads(syncTime);

        return count;
    }

    public void syncReads(final long syncTime)
            throws IOException, ReaderException {
        // NOTE: sync server read
        getClient().handleItemList(new ReaderClient.ItemListHandler() {
            @Override
            public String getSubUid() {
                return null;
            }
            @Override
            public boolean isStateRead() {
                return true;
            }
            @Override
            public int getLimit() {
                return 20;
            }
            @Override
            public long getStartTime() {
                return 0;
            }
            @Override
            public boolean isNewer() {
                return true;
            }
            @Override
            public boolean isExcludeRead() {
                return false;
            }
            @Override
            public boolean item(Item item) throws ReaderException {
                return syncServerRead(item, syncTime);
            }
        }, syncTime);

        // NOTE: sync local reads
        syncLocalReads();
    }

    public boolean syncServerRead(Item item, long syncTime) {
        Item curItem = getItemByUid(item.getUid());
        if (curItem == null) {
            return false;
        }
        if (curItem.isRead()) {
            return true;
        }
        ContentResolver cr = this.context.getContentResolver();
        ContentValues v = new ContentValues();
        v.put(Item._READ, 1);
        v.put(Item._READ_TIME, syncTime);
        v.put(Item._SYNC_TIME, syncTime);
        Uri uri = ContentUris.withAppendedId(Item.CONTENT_URI, curItem.getId());
        cr.update(uri, v, null, null);
        return true;
    }

    public boolean syncLocalReads() throws IOException, ReaderException {
        StringBuilder buff = new StringBuilder(128);
        buff.append(Item._SYNC_TIME).append(" < ").append(Item._READ_TIME);
        buff.append(" and ");
        buff.append(Item._READ).append(" = 1");
        String[] s = new String[]{Item._UID, Item._READ_TIME};
        String w = new String(buff);
        ContentResolver cr = this.context.getContentResolver();
        Item.FilterCursor csr = new Item.FilterCursor(cr.query(
            Item.CONTENT_URI, s, w, null, null));
        try {
            while (csr.moveToNext()) {
                syncLocalRead(csr.getUid(), csr.getReadTime());
            }
        } finally {
            csr.close();
        }
        return true;
    }

    public boolean syncLocalRead(Item item) throws IOException, ReaderException {
        if (item.getSyncTime() == item.getReadTime()) {
            return false;
        }
        return syncLocalRead(item.getUid(), item.getReadTime());
    }

    public boolean syncLocalRead(String itemUid, long itemReadTime)
            throws IOException, ReaderException {
        if (getClient().markAsRead(itemUid)) {
            ContentResolver cr = this.context.getContentResolver();
            String w = Item._UID + " = ?";
            String[] wa = new String[]{itemUid};
            ContentValues values = new ContentValues();
            values.put(Item._SYNC_TIME, itemReadTime);
            cr.update(Item.CONTENT_URI, values, w, wa);
            return true;
        }
        return false;
    }

    public void syncSubs(final long syncTime)
            throws IOException, ReaderException {
        ReaderClient client = getClient();

        // NOTE: sync tag
        client.handleTagList(new ReaderClient.TagListHandler() {
            @Override
            public boolean tag(Tag tag) throws ReaderException {
                syncTag(tag, syncTime);
                return true;
            }
        }, syncTime);

        // NOTE: sync subscription
        client.handleSubList(new ReaderClient.SubListHandler() {
            @Override
            public boolean subscription(Subscription sub)
                    throws ReaderException {
                syncSub(sub, syncTime);
                return true;
            }
        }, syncTime);

        // NOTE: sync unread count
        client.handleUnreadCount(new ReaderClient.UnreadCountHandler() {
            @Override
            public boolean unreadCount(String uid, int count,
                    long newestItemTime) throws ReaderException {
                syncUnreadCount(uid, count, newestItemTime);
                return true;
            }
        }, syncTime);

        ContentResolver cr = this.context.getContentResolver();
        String w = Tag._SYNC_TIME + " <> ?";
        String[] wa = new String[]{String.valueOf(syncTime)};
        cr.delete(Tag.CONTENT_URI, w, wa);

        w = Subscription._SYNC_TIME + " <> ?";
        cr.delete(Subscription.CONTENT_URI, w, wa);

        cr.query(ReaderProvider.URI_TXN_BEGIN, null, null, null, null);
        try {
            w = Tag2Sub._SYNC_TIME + " <> ?";
            cr.delete(Tag2Sub.CONTENT_URI, w, wa);

            // NOTE: folder or tag
            StringBuilder buff = new StringBuilder(128);
            buff.append("uid in (");
            buff.append("select distinct ").append(Tag2Sub._TAG_UID);
            buff.append(" from ").append(Tag2Sub.TABLE_NAME);
            buff.append(") and ");
            buff.append("type = ").append(Tag.TYPE_TAG_LABEL);

            ContentValues v = new ContentValues();
            v.put(Tag._TYPE, Tag.TYPE_FOLDER);

            cr.update(Tag.CONTENT_URI, v, new String(buff), null);

            cr.query(ReaderProvider.URI_TXN_SUCCESS, null, null, null, null);
        } finally {
            cr.query(ReaderProvider.URI_TXN_END, null, null, null, null);
        }

        Prefs.setUnreadCount(this.context, countUnread());

        this.context.sendBroadcast(new Intent(
            ReaderService.ACTION_SYNC_SUBS_FINISHED));
    }

    private void syncTag(Tag tag, long syncTime) {
        ContentResolver cr = this.context.getContentResolver();
        ContentValues values = tag.toContentValues();
        values.put(Tag._SYNC_TIME, syncTime);
        Tag curTag = getTagByUid(tag.getUid());
        if (curTag == null) {
            cr.insert(Tag.CONTENT_URI, values);
        } else {
            Uri uri = ContentUris.withAppendedId(
                Tag.CONTENT_URI, curTag.getId());
            values.remove(Tag._TYPE);
            cr.update(uri, values, null, null);
        }
    }

    private void syncSub(Subscription sub, long syncTime) {
        ContentResolver cr = this.context.getContentResolver();
        ContentValues v = sub.toContentValues();
        v.put(Subscription._SYNC_TIME, syncTime);
        long subId = getSubIdByUid(sub.getUid());
        if (subId == 0) {
            try {
                byte[] icon = client.getFavicon(sub);
                if (icon != null) {
                    v.put(Subscription._ICON, icon);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            Uri uri = cr.insert(Subscription.CONTENT_URI, v);
            subId = ContentUris.parseId(uri);
        } else {
            Uri uri = ContentUris.withAppendedId(
                Subscription.CONTENT_URI, subId);
            cr.update(uri, v, null, null);
        }

        List<String> categories = sub.getCategories();
        if (categories != null) {
            String w = Tag2Sub._SUB_ID + " = ? and " + Tag2Sub._TAG_UID + " = ?";
            String[] wa = new String[]{String.valueOf(subId), null};
            for (String category: categories) {
                v.clear();
                v.put(Tag2Sub._SUB_ID, subId);
                v.put(Tag2Sub._TAG_UID, category);
                v.put(Tag2Sub._SYNC_TIME, syncTime);
                wa[1] = category;
                if (subId == 0
                        || cr.update(Tag2Sub.CONTENT_URI, v, w, wa) == 0) {
                    cr.insert(Tag2Sub.CONTENT_URI, v);
                }
            }
        }
    }

    private void syncUnreadCount(String uid, int count, long newestItemTime) {
        ContentResolver cr = this.context.getContentResolver();
        ContentValues v = new ContentValues();
        if (uid.startsWith("feed/")) {
            v.put(Subscription._UNREAD_COUNT, count);
            v.put(Subscription._NEWEST_ITEM_TIME, newestItemTime);
            String w = Subscription._UID + " = ?";
            String[] wa = new String[]{uid};
            cr.update(Subscription.CONTENT_URI, v, w, wa);
        } else if (uid.indexOf("/label/") != -1) {
            v.put(Tag._UNREAD_COUNT, count);
            String w = Tag._UID + " = ?";
            String[] wa = new String[]{uid};
            cr.update(Tag.CONTENT_URI, v, w, wa);
        }
    }

    public int syncItems(final Subscription sub, final long syncTime)
            throws IOException, ReaderException {
        return syncItems(sub, syncTime, false);
    }

    public int syncItems(final Subscription sub, final long syncTime,
            final boolean manual) throws IOException, ReaderException {
        syncTagActions(syncTime);

        ReaderClient client = getClient();
        final int[] counter = new int[1];
        long newestItemTime = sub.getNewestItemTime();

        if (manual || !Prefs.isSyncUnreadOnly(this.context)) {
            // NOTE: sync items (all read/unread)
            client.handleItemList(new ReaderClient.ItemListHandler() {
                @Override
                public String getSubUid() {
                    return sub.getUid();
                }
                @Override
                public boolean isStateRead() {
                    return false;
                }
                @Override
                public int getLimit() {
                    return 20;
                }
                @Override
                public long getStartTime() {
                    if (manual) {
                        return 0;
                    } else {
                        return sub.getItemSyncTime();
                    }
                }
                @Override
                public boolean isNewer() {
                    return true;
                }
                @Override
                public boolean isExcludeRead() {
                    return false;
                }
                @Override
                public boolean item(Item item) throws ReaderException {
                    item.setSubId(sub.getId());
                    if (!syncItem(item, syncTime, counter, sub, false)) {
                        return false;
                    }
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        return false;
                    }
                    return true;
                }
            }, syncTime);
        }

        int limit = sub.getUnreadCount();
        if (limit > 0 || manual) {
            final int l = Math.min(limit, Prefs.getSyncItemLimit(this.context));
            // NOTE: sync items (unread only)
            client.handleItemList(new ReaderClient.ItemListHandler() {
                @Override
                public String getSubUid() {
                    return sub.getUid();
                }
                @Override
                public boolean isStateRead() {
                    return false;
                }
                @Override
                public int getLimit() {
                    return l;
                }
                @Override
                public long getStartTime() {
                    return 0;
                }
                @Override
                public boolean isNewer() {
                    return true;
                }
                @Override
                public boolean isExcludeRead() {
                    return true;
                }
                @Override
                public boolean item(Item item) throws ReaderException {
                    item.setSubId(sub.getId());
                    if (!syncItem(item, syncTime, counter, sub, true)) {
                        return false;
                    }
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        return false;
                    }
                    return true;
                }
            }, syncTime);
        }

        Uri subUri = sub.getContentUri();
        ContentValues v = new ContentValues();
        v.put(Subscription._ITEM_SYNC_TIME, syncTime / 1000);
        if (newestItemTime == 0) {
            v.put(Subscription._NEWEST_ITEM_TIME,
                sub.getNewestItemTime());
        }
        ContentResolver cr = this.context.getContentResolver();
        cr.update(subUri, v, null, null);

        if (manual) {
            updateUnreads();
        }

        return counter[0];
    }

    private void syncTagActions(long syncTime)
            throws IOException, ReaderException {
        ReaderClient client = getClient();
        ContentResolver cr = this.context.getContentResolver();
        String[] s = new String[]{"item.uid", "tag2item.tag_uid",
            "tag2item.action", "tag2item._id"};
        String w = "item._id = tag2item.item_id and tag2item.action <> 0";
        Cursor csr = cr.query(Tag2Item.CONTENT_URI, s, w, null, null);
        if (csr == null) {
            return;
        }
        try {
            ContentValues v = new ContentValues();
            while (csr.moveToNext()) {
                String itemUid = csr.getString(0);
                String tagUid = csr.getString(1);
                int action = csr.getInt(2);
                long id = csr.getLong(3);
                boolean add = action == Tag2Item.ACTION_ADD;
                client.editItemTag(itemUid, tagUid, add);
                Uri uri = ContentUris.withAppendedId(Tag2Item.CONTENT_URI, id);
                if (add) {
                    v.put(Tag2Item._ACTION, 0);
                    v.put(Tag2Item._SYNC_TIME, syncTime);
                    cr.update(uri, v, null, null);
                    v.clear();
                } else {
                    cr.delete(uri, null, null);
                }
            }
        } finally {
            csr.close();
        }
    }

    private boolean syncItem(Item item, long syncTime, int[] counter,
            Subscription sub, boolean unreadUpdate) {
        String itemUid = item.getUid();
        if (itemUid == null) {
            Log.w(TAG, "invalid item: " + item);
            return true;
        }

        ContentResolver cr = this.context.getContentResolver();
        cr.query(ReaderProvider.URI_TXN_BEGIN, null, null, null, null);
        try {
            ContentValues v = item.toContentValues();

            long itemId = getItemIdByUid(itemUid);
            if (itemId == 0) {
                v.put(Item._SYNC_TIME, syncTime);
                if (item.isRead()) {
                    v.put(Item._READ_TIME, syncTime);
                }
                Uri uri = cr.insert(Item.CONTENT_URI, v);
                itemId = ContentUris.parseId(uri);
                counter[0]++;
            } else if (unreadUpdate) {
                item.setReadTime(0);
                item.setRead(false);
                v.put(Item._READ_TIME, 0);
                v.put(Item._READ, 0);
                Uri uri = ContentUris.withAppendedId(Item.CONTENT_URI, itemId);
                cr.update(uri, v, null, null);
            }

            if (sub != null) {
                long t = v.getAsLong(Item._PUBLISHED_TIME);
                sub.setNewestItemTime(Math.max(t, sub.getNewestItemTime()));
            }

            List<String> categories = item.getCategories();
            if (categories != null) {
                String w = Tag2Item._ITEM_ID + " = ? and "
                    + Tag2Item._TAG_UID + " = ?";
                String[] wa = new String[]{String.valueOf(itemId), null};
                for (String category: categories) {
                    v.clear();
                    v.put(Tag2Item._ITEM_ID, itemId);
                    v.put(Tag2Item._TAG_UID, category);
                    v.put(Tag2Item._SYNC_TIME, syncTime);
                    wa[1] = category;
                    if (cr.update(Tag2Item.CONTENT_URI, v, w, wa) == 0) {
                        cr.insert(Tag2Item.CONTENT_URI, v);
                    }
                }
            }

            String w = Tag2Item._SYNC_TIME + " <> ? and "
                + Tag2Item._ITEM_ID + " = ?";
            String[] wa = new String[]{String.valueOf(syncTime),
                String.valueOf(itemId)};
            cr.delete(Tag2Item.CONTENT_URI, w, wa);

            cr.query(ReaderProvider.URI_TXN_SUCCESS, null, null, null, null);
            return true;
        } finally {
            cr.query(ReaderProvider.URI_TXN_END, null, null, null, null);
        }
    }

    public void removeLocalReads(long syncTime) {
        int days = Prefs.getCacheAutoCleanupDays(this.context);
        if (days < 0) {
            return;
        }
        long removeTime = syncTime - ((long) days * 24 * 60 * 60 * 1000);
        StringBuilder buff = new StringBuilder(128);
        buff.append("item.read_time <> 0 and item.read_time < ? and item.read = 1");
        if (Prefs.isCacheKeepTagged(this.context)) {
            buff.append(" and not exists (select tag2item._id from tag2item where tag2item.item_id = item._id)");
        }
        String w = new String(buff);
        String[] wa = new String[]{String.valueOf(removeTime)};
        ContentResolver cr = this.context.getContentResolver();
        cr.delete(Item.CONTENT_URI, w, wa);
    }

    public Subscription getSubBy(String column, String value) {
        return getSubBy(this.context, column, value);
    }

    public Subscription getSubById(long id) {
        return getSubBy(Subscription._ID, String.valueOf(id));
    }

    public Subscription getSubByUid(String uid) {
        return getSubBy(Subscription._UID, uid);
    }

    public Subscription getSubByUri(Uri uri) {
        return getSubBy(this.context, uri);
    }

    public Subscription getSubBySortid(String sortid) {
        return getSubBy(Subscription._SORTID, sortid);
    }

    public long getSubIdByUid(String uid) {
        ContentResolver cr = this.context.getContentResolver();
        String w = Subscription._UID + " = ?";
        String[] wa = new String[]{uid};
        Cursor csr = cr.query(Subscription.CONTENT_URI, Subscription.SELECT_ID,
            w, wa, null);
        if (csr == null) {
            return 0;
        }
        try {
            if (csr.moveToNext()) {
                return csr.getLong(0);
            }
        } finally {
            csr.close();
        }
        return 0;
    }

    public String getSubTitleById(long subId) {
        ContentResolver cr = this.context.getContentResolver();
        String[] s = new String[]{Subscription._TITLE};
        String w = Subscription._ID + " = ?";
        String[] wa = new String[]{String.valueOf(subId)};
        Cursor csr = cr.query(Subscription.CONTENT_URI, s,
            w, wa, null);
        if (csr == null) {
            return null;
        }
        try {
            if (csr.moveToNext()) {
                return csr.getString(0);
            }
        } finally {
            csr.close();
        }
        return null;
    }

    public Tag getTagByUid(String uid) {
        return getTagBy(this.context, Tag._UID, uid);
    }

    public Item getItemByUid(String uid) {
        return getItemBy(this.context, Item._UID, uid);
    }

    public Item getItemByUri(Uri uri) {
        return getItemBy(this.context, uri);
    }

    public long getItemIdByUid(String uid) {
        ContentResolver cr = this.context.getContentResolver();
        String w = Item._UID + " = ?";
        String[] wa = new String[]{uid};
        Cursor csr = cr.query(Item.CONTENT_URI, Item.SELECT_ID, w, wa, null);
        if (csr == null) {
            return 0;
        }
        try {
            if (csr.moveToNext()) {
                return csr.getLong(0);
            }
        } finally {
            csr.close();
        }
        return 0;
    }

    public int countUnread() {
        ContentResolver cr = this.context.getContentResolver();
        Cursor csr = cr.query(Subscription.CONTENT_URI,
            Subscription.SELECT_SUM_UNREAD_COUNT, null, null, null);
        try {
            return (csr.moveToNext() ? csr.getInt(0): 0);
        } finally {
            csr.close();
        }
    }

    public int countUnread(long subId) {
        ContentResolver cr = this.context.getContentResolver();
        String w = Item._SUB_ID + " = ? and " + Item._READ + " = 0";
        String[] wa = new String[]{String.valueOf(subId)};
        Cursor csr = cr.query(Item.CONTENT_URI,
            Item.SELECT_COUNT, w, wa, null);
        try {
            return (csr.moveToNext() ? csr.getInt(0): 0);
        } finally {
            csr.close();
        }
    }

    public boolean markAsRead(long[] itemIds)
            throws IOException, ReaderException {
        if (itemIds == null || itemIds.length == 0) {
            return false;
        }

        int buffSize = 128 + (itemIds.length * 3);
        StringBuilder where = new StringBuilder(buffSize);
        where.append(Item._READ + " = 0");
        where.append(" and ");
        where.append(Item._ID + " in (");
        boolean first = true;
        for (long itemId: itemIds) {
            if (first) {
                first = false;
            } else {
                where.append(",");
            }
            where.append(itemId);
        }
        where.append(")");

        ContentValues v = new ContentValues();
        v.put(Item._READ, 1);
        v.put(Item._READ_TIME, System.currentTimeMillis());

        ContentResolver cr = this.context.getContentResolver();
        cr.query(ReaderProvider.URI_TXN_BEGIN, null, null, null, null);
        try {
            cr.update(Item.CONTENT_URI, v, new String(where), null);

            updateUnreads();

            cr.query(ReaderProvider.URI_TXN_SUCCESS, null, null, null, null);
        } finally {
            cr.query(ReaderProvider.URI_TXN_END, null, null, null, null);
        }

        this.context.sendBroadcast(
            new Intent(ReaderService.ACTION_UNREAD_MODIFIED));

        return true;
    }

    public void syncMarkAllAsRead(long syncTime)
            throws IOException, ReaderException {
        ContentResolver cr = this.context.getContentResolver();

        cr.query(ReaderProvider.URI_TXN_BEGIN, null, null, null, null);
        try {
            ContentValues v = new ContentValues();
            v.put(Item._READ, 1);
            // NOTE: same readTime, syncTime
            v.put(Item._READ_TIME, syncTime);
            v.put(Item._SYNC_TIME, syncTime);

            StringBuffer buff = new StringBuffer(128);
            buff.append(Item._READ).append(" = 0");
            cr.update(Item.CONTENT_URI, v, new String(buff), null);

            updateUnreads();

            getClient().markAllAsRead(null, null, syncTime);
            cr.query(ReaderProvider.URI_TXN_SUCCESS, null, null, null, null);
        } finally {
            cr.query(ReaderProvider.URI_TXN_END, null, null, null, null);
        }

        this.context.sendBroadcast(
            new Intent(ReaderService.ACTION_UNREAD_MODIFIED));
    }

    public void syncMarkAllAsRead(Subscription sub, long syncTime)
            throws IOException, ReaderException {
        ContentResolver cr = this.context.getContentResolver();

        cr.query(ReaderProvider.URI_TXN_BEGIN, null, null, null, null);
        try {
            ContentValues v = new ContentValues();
            v.put(Item._READ, 1);
            // NOTE: same readTime, syncTime
            v.put(Item._READ_TIME, syncTime);
            v.put(Item._SYNC_TIME, syncTime);

            StringBuffer buff = new StringBuffer(128);
            buff.append(Item._READ).append(" = 0");
            buff.append(" and ");
            buff.append(Item._SUB_ID).append(" = ").append(sub.getId());
            cr.update(Item.CONTENT_URI, v, new String(buff), null);

            updateUnreads();

            getClient().markAllAsRead(sub.getUid(), sub.getTitle(), syncTime);
            cr.query(ReaderProvider.URI_TXN_SUCCESS, null, null, null, null);
        } finally {
            cr.query(ReaderProvider.URI_TXN_END, null, null, null, null);
        }

        this.context.sendBroadcast(
            new Intent(ReaderService.ACTION_UNREAD_MODIFIED));
    }

    public void syncMarkAllAsRead(Tag tag, long syncTime)
            throws IOException, ReaderException {
        if (tag.getType() == Tag.TYPE_FOLDER) {
            Query q = getSubQueryByTagUid(tag.getUid(), true);
            Subscription.FilterCursor csr = new Subscription.FilterCursor(
                q.query(this.context));
            while (csr.moveToNext()) {
                Subscription sub = csr.getSubscription();
                syncMarkAllAsRead(sub, syncTime);
            }
            return;
        }
        ContentResolver cr = this.context.getContentResolver();

        cr.query(ReaderProvider.URI_TXN_BEGIN, null, null, null, null);
        try {
            ContentValues v = new ContentValues();
            v.put(Item._READ, 1);
            // NOTE: same readTime, syncTime
            v.put(Item._READ_TIME, syncTime);
            v.put(Item._SYNC_TIME, syncTime);

            Query q = getItemQueryByTagUid(tag.getUid(), true);
            q.setSelection(new String[]{Item.TABLE_NAME + "." + Item._ID});
            Cursor csr = q.query(this.context);
            while (csr.moveToNext()) {
                long itemId = csr.getInt(0);
                Uri uri = ContentUris.withAppendedId(Item.CONTENT_URI, itemId);
                cr.update(uri, v, null, null);
            }

            updateUnreads();

            getClient().markAllAsRead(tag.getUid(), tag.getLabel(), syncTime);
            cr.query(ReaderProvider.URI_TXN_SUCCESS, null, null, null, null);
        } finally {
            cr.query(ReaderProvider.URI_TXN_END, null, null, null, null);
        }

        this.context.sendBroadcast(
            new Intent(ReaderService.ACTION_UNREAD_MODIFIED));
    }

    private void updateUnreads() {
        ContentResolver cr = this.context.getContentResolver();
        cr.update(ReaderProvider.URI_UPDATE_UNREADS, null, null, null);
        Prefs.setUnreadCount(this.context, countUnread());
    }

    public boolean editItemStar(Item item, boolean add) {
        Tag tag = getTagBy(this.context, Tag._TYPE,
            String.valueOf(Tag.TYPE_TAG_STARRED));
        if (tag == null) {
            return false;
        }
        return editItemTag(item, tag.getUid(), add);
    }

    public boolean editItemTag(Item item, String tagUid, boolean add) {
        ContentResolver cr = this.context.getContentResolver();
        String w = Tag2Item._TAG_UID + " = ? and " + Tag2Item._ITEM_ID + " = ?";
        String[] wa = new String[]{tagUid, String.valueOf(item.getId())};
        Cursor csr = cr.query(Tag2Item.CONTENT_URI, Tag2Item.SELECT_ID,
            w, wa, null);
        try {
            ContentValues v = new ContentValues();
            int action = (add) ? Tag2Item.ACTION_ADD: Tag2Item.ACTION_REMOVE;
            v.put(Tag2Item._ACTION, action);

            if (csr.moveToNext()) {
                long id = csr.getLong(0);
                Uri uri = ContentUris.withAppendedId(Tag2Item.CONTENT_URI, id);
                cr.update(uri, v, null, null);
            } else if (add) {
                v.put(Tag2Item._ITEM_ID, item.getId());
                v.put(Tag2Item._TAG_UID, tagUid);
                v.put(Tag2Item._SYNC_TIME, 0);
                cr.insert(Tag2Item.CONTENT_URI, v);
            }
        } finally {
            csr.close();
        }
        return true;
    }
}
